PostCSS: the Future after Sass and Less

Andrey Sitnik, Evil Martians

PostCSS the Future after Sass and Less

logo.svg Andrey Sitnik, Evil Martians

martians.svg

Worked on

ebay.svggroupon.svgamplifr.png

Open Source

autoprefixer.svglogo.svg

Part 1 Problem

covers/problem.jpg

Evolution

Mutations
Selection
Inheritance

Natural Selection?

<blink> was supported for 19 years

Random Mutations

Freedom is not worth having if it does not include the freedom to make mistakes

— Mahatma Gandhi

40% of ES6 comes from CoffeeScript

Part 2 Preprocessors

covers/sass.jpg

CSS Templating

a {
    <%= include clickable %>
    color: <%= $link-color %>;
}

Syntax Features

  1. Variables
    $var: 1
  2. Mixins
    @include mixin
  3. Functions
    black()

Problem 1 Limitations

a {
    width: 20rem;
}

Problem 2 Monolithic

Problem 3 Hard to Code

@import "compass/support";

// The the user threshold for transition support. Defaults to `$graceful-usage-threshold`
$transition-support-threshold: $graceful-usage-threshold !default;


// CSS Transitions
// Currently only works in Webkit.
//
// * expected in CSS3, FireFox 3.6/7 and Opera Presto 2.3
// * We'll be prepared.
//
// Including this submodule sets following defaults for the mixins:
//
//     $default-transition-property : all
//     $default-transition-duration : 1s
//     $default-transition-function : false
//     $default-transition-delay    : false
//
// Override them if you like. Timing-function and delay are set to false for browser defaults (ease, 0s).

$default-transition-property: all !default;

$default-transition-duration: 1s !default;

$default-transition-function: null !default;

$default-transition-delay: null !default;

$transitionable-prefixed-values: transform, transform-origin !default;



// Checks if the value given is a unit of time.
@function is-time($value) {
  @return if(type-of($value) == number, not not index(s ms, unit($value)), false);
}

// Returns `$property` with the given prefix if it is found in `$transitionable-prefixed-values`.
@function prefixed-for-transition($prefix, $property) {
  @if not $prefix {
    @return $property;
  }
  @if type-of($property) == list or type-of($property) == arglist {
    $new-list: comma-list();
    @each $v in $property {
      $new-list: append($new-list, prefixed-for-transition($prefix, $v));
    }
    @return $new-list;
  } @else {
    @if index($transitionable-prefixed-values, $property) {
      @return #{$prefix}-#{$property};
    } @else {
      @return $property;
    }
  }
}

// Returns $transition-map which includes key and values that map to a transition declaration
@function transition-map($transition) {
  $transition-map: ();

  @each $item in $transition {
    @if is-time($item) {
      @if map-has-key($transition-map, duration) {
        $transition-map: map-merge($transition-map, (delay: $item));
      } @else {
        $transition-map: map-merge($transition-map, (duration: $item));
      }
    } @else if map-has-key($transition-map, property) {
      $transition-map: map-merge($transition-map, (timing-function: $item));
    } @else {
      $transition-map: map-merge($transition-map, (property: $item));
    }
  }

  @return $transition-map;
}

// One or more properties to transition
//
// * for multiple, use a comma-delimited list
// * also accepts "all" or "none"

@mixin transition-property($properties...) {
  $properties: set-arglist-default($properties, $default-transition-property);
  @include with-each-prefix(css-transitions, $transition-support-threshold) {
    $props: if($current-prefix, prefixed-for-transition($current-prefix, $properties), $properties);
    @include prefix-prop(transition-property, $props);
  }
}

// One or more durations in seconds
//
// * for multiple, use a comma-delimited list
// * these durations will affect the properties in the same list position

@mixin transition-duration($durations...) {
  $durations: set-arglist-default($durations, $default-transition-duration);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-duration: $durations
  ));
}

// One or more timing functions
//
// * [ ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier(x1, y1, x2, y2)]
// * For multiple, use a comma-delimited list
// * These functions will effect the properties in the same list position

@mixin transition-timing-function($functions...) {
  $functions: set-arglist-default($functions, $default-transition-function);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-timing-function: $functions
  ));
}

// One or more transition-delays in seconds
//
// * for multiple, use a comma-delimited list
// * these delays will effect the properties in the same list position

@mixin transition-delay($delays...) {
  $delays: set-arglist-default($delays, $default-transition-delay);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-delay: $delays
  ));
}

// Transition all-in-one shorthand

@mixin single-transition(
  $property: $default-transition-property,
  $duration: $default-transition-duration,
  $function: $default-transition-function,
  $delay: $default-transition-delay
) {
  @include transition(compact($property $duration $function $delay));
}

@mixin transition($transitions...) {
  $default: (compact($default-transition-property $default-transition-duration $default-transition-function $default-transition-delay),);
  $transitions: if(length($transitions) == 1 and type-of(nth($transitions, 1)) == list and list-separator(nth($transitions, 1)) == comma, nth($transitions, 1), $transitions);
  $transitions: set-arglist-default($transitions, $default);


  @include with-each-prefix(css-transitions, $transition-support-threshold) {
    $delays: comma-list();
    $transitions-without-delays: comma-list();
    $transitions-with-delays: comma-list();
    $has-delays: false;


    // This block can be made considerably simpler at the point in time that
    // we no longer need to deal with the differences in how delays are treated.
    @each $transition in $transitions {
      // Declare initial values for transition
      $transition: transition-map($transition);

      $property: map-get($transition, property);
      $duration: map-get($transition, duration);
      $timing-function: map-get($transition, timing-function);
      $delay: map-get($transition, delay);

      // Parse transition string to assign values into correct variables
      $has-delays: $has-delays or $delay;

      @if $current-prefix == -webkit {
        // Keep a list of delays in case one is specified
        $delays: append($delays, if($delay, $delay, 0s));
        $transitions-without-delays: append($transitions-without-delays,
          prefixed-for-transition($current-prefix, $property) $duration $timing-function);
      } @else {
        $transitions-with-delays: append($transitions-with-delays,
          prefixed-for-transition($current-prefix, $property) $duration $timing-function $delay);
      }
    }

    @if $current-prefix == -webkit {
      @include prefix-prop(transition, $transitions-without-delays);
      @if $has-delays {
        @include prefix-prop(transition-delay, $delays);
      }
    } @else if $current-prefix {
      @include prefix-prop(transition, $transitions-with-delays);
    } @else {
      transition: $transitions-with-delays;
    }
  }
}

Part 3 PostCSS

covers/postcss.jpg

The Beginning

tj.jpgModular CSS Preprocessing with Rework
— TJ Holowaychuk, 2013

Development

Rework

PostCSS

PostCSS

CSS
source map
Parser
Plugin
Plugin
Stringifier
New CSS
new source map

Usage

let postcss = require('postcss');

postcss([ plugin1, plugin2 ])
    .process(css)
    .then( result => console.log(result.css) );

Plugin

function (css) {
    css.eachDecl( decl => {
        decl.value = decl.value.replace(/\d+rem/, rem => {
            return 16 * parseFloat(rem) + 'px';
        });
    });
};

Difference

Preprocessor

PostCSS

Evolution

Write a plugin
Popularity
Specification

Part 4 Practice

covers/practice.jpg

Plugins postcss-simple-vars

$blue: #056ef0;
$column: 200px;

.menu_link {
    background: $blue;
    width: $column;
}

Plugins postcss-nested

.phone {
    &_title {
        width: 500px;
        @media (max-width: 500px) {
            width: auto;
        }
    }
}

Plugins postcss-mixins

@define-mixin icon $network $color {
    .icon.is-$network {
        color: $color;
    }
}

@mixin icon twitter blue;
@mixin icon youtube red;

Maintainability

Impossible with Sass autoprefixer

:fullscreen a {
    transition: transform 1s;
}
:-webkit-full-screen a {
    -webkit-transition: -webkit-transform 1s;
            transition: transform 1s;
}
:-moz-full-screen a {
    transition: transform 1s;
}
:-ms-fullscreen a {
    transition: transform 1s;
}
:fullscreen a {
    -webkit-transition: -webkit-transform 1s;
            transition: transform 1s;
}

Impossible with Sass cssnext

@custom-selector --heading h1, h2, h3, h4, h5, h6;

.post-article --heading {
    margin-top: calc(10 * var(--row));
    color: color( var(--mainColor) blackness(+20%) );
    font-variant-caps: small-caps;
}

Impossible with Sass cssgrace

.icon {
    opacity: 0.6;
    display: inline-block;
}
.icon {
    opacity: 0.6;
    filter: alpha(opacity=60);
    display: inline-block;
    *display: inline;
    *zoom: 1;
}

Impossible with Sass postcss-quantity-queries

ul > li:exactly(4) {
    width: 25%;
}

ul > li:exactly(5) {
    width: 20%;
}
ul > li:nth-last-child(4):first-child,
ul > li:nth-last-child(4):first-child ~ li {
    width: 25%;
}


ul > li:nth-last-child(5):first-child,
ul > li:nth-last-child(5):first-child ~ li {
    width: 20%;
}

Impossible with Sass postcss-data-packer

/* style.css */
.icon1 {
    width: 100px;
    background: url(data:…);
}
.icon2 {
    background: url(data:…);
}
/* style.css */
.icon1 {
    width: 100px;
}
/* style.icons.css */
.icon1, .icon2 {
    background: url(data:…);
}

Impossible with Sass postcss-bem-linter

Lint Twitter BEM-style SUIT CSS

Impossible with Sass doiuse

Lint CSS for browser support against Can I Use database

main.css: line 15, col 3 -
  CSS user-select: none not supported by: IE (8,9)
main.css: line 32, col 3 -
  CSS3 Transforms not supported by: IE (8)

Hebrew Wikipedia

hewiki.jpg

Impossible with Sass rtlcss

Mirror styles for Arabic or Hebrew

a {
    left: 10px;
    text-align: left;
}
a {
    right: 10px;
    text-align: right;
}

70+ plugins

Performance

PostCSS
36 ms
libsass
109 ms
Less
150 ms
Stylus
283 ms
Sass
1153 ms

Performance

PostCSS
36 ms
libsass
109 ms
Less
150 ms
Stylus
283 ms
Sass
1153 ms

Benefits

  1. Performance
  2. Modularity
  3. Features that are impossible with Sass

Part 5 Present

covers/present.jpg

430 000 downloads per month

PostCSS Users

google.svgtaobao.svgwordpress.svgtwitter.svg

Buzz

alistapart.pngWhat Will Save Us from the Dark Side of Pre‑Processors
 — A List Apart

Buzz

benfrain.jpgBreaking up with Sass: it’s not you, it’s me
 — Ben Frain, author of «Sass and Compass for Designers»

Part 6 Usage

covers/use.jpg

1. CSS Tool

2. Project without PostCSS

.pipe( sass() )
.pipe( postcss([
    require('autoprefixer')
]) )

3. Project with Autoprefixer

.pipe( sass() )
.pipe( postcss([
    require('autoprefixer'),
    require('postcss-easings'),
    require('cssnext')
]) )

4. New Project

.pipe( postcss([
    require('postcss-nested'),
    require('postcss-mixins'),
    require('postcss-simple-vars'),
    require('autoprefixer'),
    require('postcss-easings'),
    require('cssnext')
]) )

Questions

covers/ask.jpg